desc:Super8 MIDI-controlled synchronized looper (Cockos)

// Copyright (C) 2015 and onward Cockos Inc
// LICENSE: LGPL v2 or later - http://www.gnu.org/licenses/lgpl.html

// General notes:
// 8 channels of audio input (1 channel per channel)
// 9 channels of audio output (1 channel per channel, 9th channel is selected-channel-only monitoring output)

// 8 channels are all synchronized. You can turn each channel on and off and overdub on each channel.

// MIDI assigments are mappable by right-click-dragging them in the UI.
// for each channel:
//   - note1=if not recording, start record. 
//           If already recording (and initial length-setting pass), sets length and continues (overdub)
//           If already recording (and length is set), goes back to playback
//   - note2=toggle playback (stop/rec->play, play->stop)
//   - note3=toggle selected-for-monitoring
//   - note4=reverse loop
// Right click the channel's monitoring icon to go through off/auto/always-on. 
//   - I use 'off' for things like mic'd drums
//   - I use 'always-on' for stuff where the source should always be audible (usually looping is less important)

// extra buttons (click, or drag them):
// - kill (click) -- deletes ALL!
// - halve length (click)
// - double length (click)
// - double length (no repeat) (click) -- if there's existing content after the current loop, it won't be overwritten
// - start gate: when recording the initial loop, use this gate value before starting recording 
//   (useful for hand-shortages)
// - halve fadesize: crossfade this amount when halving loop. 
//   If you intend to double (no repeat) back up, set to 0ms and accept the clicks.
// - add to project: adds active channels as .wav files to the end of the project, on their respective tracks, 
//   setting the tempo accordingly. (!) This uses a new undocumented (except for this) JSFX API...


in_pin:input 1
in_pin:input 2
in_pin:input 3
in_pin:input 4
in_pin:input 5
in_pin:input 6
in_pin:input 7
in_pin:input 8
out_pin:output 1
out_pin:output 2
out_pin:output 3
out_pin:output 4
out_pin:output 5
out_pin:output 6
out_pin:output 7
out_pin:output 8
out_pin:monitor output
out_pin:click output

options: maxmem=33000000 no_meter

@init

// constants
g_nchan = 8;
g_max_alloc = __memtop(); // JSFX memory available
g_latchq_len = 1024; // 1024 midi events queueable for latch mode
g_maxlen = ((g_max_alloc-4096 - g_latchq_len*3)/(g_nchan+1))|0; // use nearly all of available memory
g_fadespeed = exp(-1/(srate * 0.002));  // fade speed for playback/monitoring
g_slope_recin = 1/(srate * 0.001); // fade length for starting rec
g_slope_recout = 1/(srate * 0.030); // fade length for ending rec
g_infthreshdb = -120;
g_infthresh = 10^(g_infthreshdb/20);
g_latchmode=0;

// per-channel state/configuration record (mem_stlist)
st_monmode = 0; // mirrors chX.monmode
st_note1 = 1; // mirrors chX.note/chX.note2/chX.note3/chX.note4
st_note2 = 2;
st_note3 = 3;
st_note4 = 4; // reverse note

st_state = 5; // 0=off, 1=play, 2=recording
st_buf   = 6; // pointer to audio buffer
st_dirty = 7;  // set if buffer is dirty, and also may indicate how much of it 
               // is using max(mem_stlist[st_dirty],chX.dirty_top+1)
st_lastd = 8; // UI flags for mode + monitor + dirty
st_lastpkupd = 9; // time of last peak drawin
st_peak_in = 10;
st_rdc = 11; // msec, chX.rdc is samples
st_num = 12;


function get_section(s) local(r) ( s?(r=s):r; );

function alloc(sz) ( (this.top+=sz)-sz; );

function updatefromrec() instance(rec)
(
  this.note  = rec[st_note1];
  this.note2 = rec[st_note2];
  this.note3 = rec[st_note3];
  this.note4 = rec[st_note4];
  this.monmode = rec[st_monmode];
  this.rdc =(rec[st_rdc]*.001*samplerate+.5)|0;
);

function init(x, n1,n2,n3) 
  instance(idx rec buf monvol fpos fpos2 fpos_slope sk_fpos2 dirty_top) 
(
  idx = x;
  fpos_slope = monvol = fpos = fpos2 = sk_fpos2 = 0;
  dirty_top = -1;
  buf = alloc(g_maxlen);
  
  rec = mem_stlist + x*st_num;
  rec[st_note1] = n1;
  rec[st_note2] = n2;
  rec[st_note3] = n3;
  rec[st_note4] = 128;
  rec[st_state]=0;
  rec[st_lastd]=-1;
  rec[st_dirty]=0;
  rec[st_monmode] = 1;
  rec[st_buf] = buf;
  rec[st_peak_in] = 0;
  rec[st_rdc] = 0;
  
  this.updatefromrec();
);

function get_zero_buffer(buf,buftop) (
  buftop > 0 ? ( 
    get_section() != 'gfx' && buftop > max(8192, g_spare_buf_ztop) ? ( 
      atomic_exch(buf, g_spare_buf);
      atomic_exch(buftop, g_spare_buf_ztop);
    );
    buftop > 0 ? memset(buf,0,buftop);
  );
  buf;
);

function setstate(st) instance(fpos_slope, sk_fpos2, rec) local(dt)
(
  rec[st_state] ? (
    st==0 ? ( 
      g_active_cnt -= 1; 
    ) : g_length > 0 ? (
      g_firstrec = 0;   
      g_recstart_gate = 0;
      this.extra_recording = 0|min(srate*.5, g_maxlen-g_length);
    );
  ) : (
    st>0 ? ( 
      st == 2 && (dt = max(rec[st_dirty], this.dirty_top+1)) > 0 ? (
        this.buf = rec[st_buf] = get_zero_buffer(this.buf,dt);
        this.dirty_top=-1;
        rec[st_dirty]=0;
      );
      
      (g_active_cnt += 1) == 1 ? (
        st == 2 ? ( 
          g_recstart_gate = mem_gen_cfg[4] > g_infthreshdb ? ( 10^(mem_gen_cfg[4]/20) );
          g_pos = 0;
          g_offs = g_length = 0; 
          g_firstrec = 1; 
        );        
      );
    );
  );
  rec[st_state] = st;
  rec[st_dirty] = max(max(rec[st_dirty],this.dirty_top+1),st==2);
  
  st == 2 ? (
    fpos_slope = g_slope_recin;
    sk_fpos2 = 1 - g_fadespeed;
  ) : (
    fpos_slope = -g_slope_recout;
    sk_fpos2 = st > 0 ? (1 - g_fadespeed) : 0;
  );
);


function process(s) instance(fpos, fpos2, monvol, monmode, rec, peak_in) local(r,wrpos,buf) (
  g_recstart_gate==0 ? (
    fpos = min(max(0,fpos+this.fpos_slope),1);
    fpos2 = fpos2*g_fadespeed + this.sk_fpos2;
  ) : (
    fpos = fpos2 = 0;
  );
  peak_in = max(abs(s),peak_in);

  wrpos = g_pos;
  buf = this.buf + g_offs;
  g_firstrec == 0 ? (
    this.extra_recording > 0 ? (
      buf[g_length + g_pos] = s; // write past the end of the loop (in case we want to trim later)
      this.dirty_top = max(this.dirty_top,g_length + g_pos + g_offs);
      this.extra_recording -= 1;
    );

    this.rdc < g_length ? (
      wrpos -= this.rdc;
      wrpos < 0 ? wrpos += g_length;
    );
  );
  monmode == 0 ? (
    // no monitoring
    fpos > 0.0001 ? (
      this.dirty_top = max(this.dirty_top,wrpos + g_offs);
      r=buf[g_pos]*fpos2;
      buf[wrpos] += s*fpos;
      r;
    ) : fpos2 > 0.0001 ? (
      buf[g_pos]*fpos2;
    ) : 0;
    
  ) : ( 
    (monmode>=2 || g_chan_selected == this.idx) ? (
      monvol = 1 + (monvol-1)*g_fadespeed;
    ) : ( 
      monvol *= g_fadespeed;
    );
    
    fpos > 0.0001 ? (
      this.dirty_top = max(this.dirty_top,wrpos + g_offs);
      wrpos == g_pos ? (
        (buf[g_pos] += s*fpos) * fpos2 + s * max(0, monvol - fpos*fpos2);
      ) : (
        buf[wrpos] += s*fpos;
        buf[g_pos] * fpos2 + s * monvol;
      );
    ) : fpos2 > 0.0001 ? (
      buf[g_pos]*fpos2 + s*monvol;
    ) : (
      s*monvol;
    );
  );
);

function reverse(want_defer) instance(rec) local(i,x,tmp)
(
  // reverse
  i=this.buf + g_offs;
  x=i + g_length-1;
  loop(g_length/2,
    tmp=i[0];
    i[0]=x[0];
    x[0]=tmp;
    x-=1;
    i+=1;
  );
  rec[st_dirty] = this.dirty_top = max(rec[st_dirty],max(this.dirty_top,g_offs+g_length));     
  rec[st_lastd] = -1;
);

function onmsg(m1,m2,m3) instance(rec,note,note2,note3,note4) (
   m2 == note || m2 == 1024+rec+st_note1 ? (
     this.setstate((rec[st_state] == 2 && g_firstrec==0) ? 1 : 2);
     g_chan_selected = this.idx;
   ) : m2 == note2  || m2 == 1024+rec+st_note2 ? (
     this.setstate(rec[st_state] == 2 || (rec[st_state]==0 && g_length) ? 1 : 0);
     g_chan_selected = this.idx;
   ) : m2 == note3 ? (
     g_chan_selected = (g_chan_selected == this.idx) ? -1 : this.idx;
   ) : m2 == note4 || m2 == 1024+rec+st_note4 ? (
     this.reverse(1);
   );
);

function redraw_channels() local(p)
(
  p=mem_stlist+st_lastd;
  loop(g_nchan,
    p[0] = -1;
    p+=st_num;
  );
);

function repeatbuf(buf,osz, nsz) local(p,s)
(
  p=buf+osz;
  while (p < buf+nsz) 
  (
    s = min(buf+nsz-p,osz);
    memcpy(p,buf,s);
    p+=s;
  ); 
);

function xfadebuf(buf, osz, nsz) local(fsz,s,ds)
(
  fsz = (0.001*mem_gen_cfg[5]*srate)|0;
  s=0;
  osz>0 && osz == nsz ? osz += fsz;
  fsz = min(fsz,osz-nsz)|0;
  ds=1/fsz;
  // fade buf[0..fsz] with buf[nsz..nsz+fsz] 
  loop(fsz,
    buf[0] = buf[0] * s + buf[nsz]*(1-s);
    s+=ds;
    buf+=1;
  );
 
);

function adjustsizes(scale, repup) local(nlen,olen, lp)
(
  olen = g_length;
  nlen = (olen*scale)|0;
  nlen >= 1 && nlen < g_maxlen ? (
    lp=mem_stlist;
    loop(g_nchan,
      lp[st_dirty] ? (
        nlen > olen ? (
          repup ? repeatbuf(lp[st_buf] + g_offs,olen,nlen);
          nlen+g_offs > lp[st_dirty] ? lp[st_dirty] = nlen+g_offs;
        ) : (
          xfadebuf(lp[st_buf] + g_offs,olen,nlen);
        );
      );
      lp += st_num;
    );
    g_length = nlen;
    g_pos %= nlen;
    redraw_channels();
  );
);

function updatechfromrec()
(
  ch1.updatefromrec();
  ch2.updatefromrec();
  ch3.updatefromrec();
  ch4.updatefromrec();
  ch5.updatefromrec();
  ch6.updatefromrec();
  ch7.updatefromrec();
  ch8.updatefromrec();
);

function reset() local(i,lp)
( 
  g_chan_selected=-1;
  ch1.setstate(0); ch1.dirty_top=-1; // setstate() will latch dirty_top to st_dirty
  ch2.setstate(0); ch2.dirty_top=-1;
  ch3.setstate(0); ch3.dirty_top=-1;
  ch4.setstate(0); ch4.dirty_top=-1;
  ch5.setstate(0); ch5.dirty_top=-1;
  ch6.setstate(0); ch6.dirty_top=-1;
  ch7.setstate(0); ch7.dirty_top=-1;
  ch8.setstate(0); ch8.dirty_top=-1;
  i=0;
  lp = mem_stlist;
  loop(g_nchan,
    lp[st_dirty] > 0 ? (
      memset(lp[st_buf], 0, lp[st_dirty]);
      lp[st_dirty] = 0;
    );
    i+=1;
    lp += st_num;
  );
  g_offs=g_length=g_pos=0;
);


function estbpm(len) local(bpm, tsnum) global(ts_num, srate, mem_gen_cfg)
(
  bpm = 120.0 * srate / len;
  (tsnum = (mem_gen_cfg[9]|0)) < 2 ? tsnum = ts_num;
  tsnum >= 2 ? bpm *= tsnum * .25;
  
  while (bpm < 60) ( bpm*=2; );
  while (bpm > 240) ( bpm/=2; );
  bpm;
);

function gen_action(i)
(
  i == 0 ? reset() : 
  i == 1 ? adjustsizes(0.5,1) : 
  i == 2 ? adjustsizes(2,1) : 
  i == 3 ? adjustsizes(2,0) : 
  i == 7 ? g_need_export=1 : 
  i == 11 ? adjustsizes(1, 0);
);


// one-time initialization
ext_noinit == 0 ? (
  ext_noinit=1;
  ext_nodenorm=1;
  
  alloc.top=32;
  
  g_latchq = alloc(g_latchq_len*3);
  g_latchq_used=0;
  
  mem_gen_sz=13;
  mem_gen_cfg = alloc(mem_gen_sz);
  memset(mem_gen_cfg,128,mem_gen_sz);
  mem_gen_cfg[4]=g_infthreshdb; // gate threshold
  mem_gen_cfg[5]=5; // msec fade when using halve
  mem_gen_cfg[6]=0;
  mem_gen_cfg[8]=0;
  mem_gen_cfg[9]=0;
  mem_gen_cfg[10]=0;
  mem_gen_cfg[12]=0;
  
  mem_gen_names = alloc(mem_gen_sz);
  mem_gen_names[0] = "kill";
  mem_gen_names[1] = "halve";
  mem_gen_names[2] = "double";
  mem_gen_names[3] = "double\nno rep";
  mem_gen_names[4] = "init\nrec\ngate";
  mem_gen_names[5] = "";  
  mem_gen_names[6] = "";  
  mem_gen_names[7] = "add to\nproject";
  mem_gen_names[8] = "";  
  mem_gen_names[9] = "";  
  mem_gen_names[10] = "";  
  mem_gen_names[11] = "x-fade\nshortened\nloop";
 
  mem_gen_order = alloc(mem_gen_sz);
  mem_gen_order[0]=0;
  mem_gen_order[1]=1;
  mem_gen_order[2]=2;
  mem_gen_order[3]=3;
  mem_gen_order[4]=7;
  mem_gen_order[5]=11;
  mem_gen_order[6]=-1;
  mem_gen_order[7]=-1;
  mem_gen_order[8]=-1;
  mem_gen_order[9]=-1;
  mem_gen_order[10]=-1;
  mem_gen_order[11]=-1;
  mem_gen_order[12]=-1;
  
  mem_stlist = alloc(g_nchan * st_num);

  g_inject_midinote = 0;
  g_need_export = 0;

  g_inactive_blockcnt=0;  
  g_active_cnt = 0; 
  g_offs = 0;
  g_length = 0;
  g_pos=0;
  g_firstrec=0;
  g_recstart_gate=0;
  g_chan_selected = -1; // currently selected channel
  g_lastmsg = -1;
  g_spare_buf = alloc(g_maxlen);
  g_spare_buf_ztop = 0;
  
  ch1.init(0, 36,37, 128);
  ch2.init(1, 38,39, 128);
  ch3.init(2, 41,42, 128);
  ch4.init(3, 43,44, 128);
  ch5.init(4, 45,46, 128);
  ch6.init(5, 48,49, 128);
  ch7.init(6, 50,51, 128);
  ch8.init(7, 53,54, 128);
);




@serialize
function doconf() local(i,s) (
  i=mem_stlist;
  loop(g_nchan,
    file_var(0,i[st_monmode]);
    i+=st_num;
  );
  
  s=mem_gen_sz; // safe to increase mem_gen_sz without breaking compat
  file_var(0,s);

  i=0;
  loop(s,
    file_var(0,i < mem_gen_sz ? mem_gen_cfg[i] : 0);
    i+=1;
  );
  
  i = mem_stlist;
  loop(g_nchan,
    file_var(0,i[st_note1]);
    file_var(0,i[st_note2]);
    file_var(0,i[st_note3]);
    i+=st_num;
  );
  
  file_avail(0) != 0 ? (
    i = mem_stlist;
    loop(g_nchan,
      file_var(0,i[st_note4]);
      i+=st_num;
    );  
  );

  file_avail(0) != 0 ? (
    i = mem_stlist;
    loop(g_nchan,
      file_var(0,i[st_rdc]);
      i+=st_num;
    );  
  );
  
  file_avail(0)>=0 ? ( // is reading config
    updatechfromrec();
    g_latchmode = mem_gen_cfg[8];
    last_w=0;
  );
);
doconf();

@block
ccmode_blockcnt+=1;
g_peak_decay = 10^(-samplesblock/srate*10);

function trimspare() local(a) (
  a = min(g_spare_buf_ztop, 65536);
  memset(g_spare_buf + (g_spare_buf_ztop -= a),0,a);
);
g_spare_buf_ztop > 0 ? trimspare();

g_active_cnt==0? (
  g_inactive_blockcnt < 2 ? ( 
    g_inactive_blockcnt+=1; 
  ) : (
    g_pos = 0
  );
) : g_inactive_blockcnt=0;

sidx=0;
nextmsg_offs=-1;
g_inject_midinote >= 1024 ? (
  nextmsg_offs = 0;
  nextmsg_1=0x90;
  nextmsg_2=g_inject_midinote;
  nextmsg_3=127;
  g_inject_midinote=0;
) : (
  midirecv(nextmsg_offs,nextmsg_1,nextmsg_2,nextmsg_3) ? midisend(nextmsg_offs,nextmsg_1,nextmsg_2,nextmsg_3);
);

function onblock() instance(rec peak_in) global(g_peak_decay st_peak_in)
(
rec[st_peak_in] = peak_in;
peak_in *= g_peak_decay;
);

ch1.onblock();
ch2.onblock();
ch3.onblock();
ch4.onblock();
ch5.onblock();
ch6.onblock();
ch7.onblock();
ch8.onblock();

(g_click_int = (mem_gen_cfg[9]|0)) > 0 ?( 
  g_click_lenspls = (0.01*srate)|0;
  g_click_fadesz = (0.0041*srate)|0;
  g_click_fadesz_i = 1/g_click_fadesz;
);

i_srate = 1.0/srate;

@sample

function chan_onmsg(m1,m2,m3)
(
  ch1.onmsg(m1,m2,m3);
  ch2.onmsg(m1,m2,m3);
  ch3.onmsg(m1,m2,m3);
  ch4.onmsg(m1,m2,m3);
  ch5.onmsg(m1,m2,m3);
  ch6.onmsg(m1,m2,m3);
  ch7.onmsg(m1,m2,m3);
  ch8.onmsg(m1,m2,m3);
);

function latchq_add(m1,m2,m3) local(v)
(
  g_latchq_used < g_latchq_len ? 
  (
    v = g_latchq_used*3;
    g_latchq[v]=m1;
    g_latchq[v+1]=m2;
    g_latchq[v+2]=m3;
    g_latchq_used+=1;
  );
);

function latchq_process() local(r)
(
  r=g_latchq;
  loop(g_latchq_used,
    chan_onmsg(r[0],r[1],r[2]);
    r+=3;
  );
  g_latchq_used=0;
);


function tonegen(x) (
  1/*change to 0 for sine*/ ? (
    x = x - floor(x);
    4 * (x < 0.25 ? x : x < 0.75 ? 0.5-x : (x-1));
  ) : sin(x*2*$pi);
);

function generate_click() 
  global(g_pos, g_length, i_srate, 
         g_click_int, g_click_lenspls, g_click_fadesz, g_click_fadesz_i)
  local(s, int)
(
  int = (g_length/g_click_int);
  s = g_pos - floor(g_pos/int)*int;
  s < g_click_lenspls ? (
    (s < g_click_lenspls-g_click_fadesz ? 1 : ( (g_click_lenspls-s)*g_click_fadesz_i )) * (
       g_pos < int ? 
         tonegen(s*800*i_srate)*1 
       :
         tonegen(s*1600*i_srate)*.75; 
    );
  );
);

g_latchq_used>0 && (g_pos == 0 || g_firstrec) ? latchq_process();

while (sidx == nextmsg_offs)
(

  // if you want to enable a CC to trigger the current channel, you could enable this:
  0 && g_chan_selected >= 0 && 
    nextmsg_1 == 0xb0 && nextmsg_3 == 0x7f && (nextmsg_2 == 0x14 || nextmsg_2 == 0x15) &&
    ccmode_blockcnt > (srate/samplesblock)*.5 ? 
  (
      ccmode_blockcnt=0;
      nextmsg_1 = 0x90;
      // if not stopped, treat as play button
      nextmsg_2 = mem_stlist[g_chan_selected*st_num + st_note1 + (mem_stlist[g_chan_selected*st_num + st_state]?1:0)];
      // nextmsg_2 = mem_stlist[g_chan_selected*st_num + st_note1 + nextmsg_2 - 0x14];
      nextmsg_3 = 100;
  );

  nextmsg_1 == 0x90 && nextmsg_3 != 0  ? (
    (!g_latchmode || g_pos == 0 || g_firstrec) ? (
      chan_onmsg(nextmsg_1,nextmsg_2,nextmsg_3);    
    ) : (
      latchq_add(nextmsg_1,nextmsg_2,nextmsg_3);
    );
    
    nextmsg_2 == mem_gen_cfg[0] ? gen_action(0);
    nextmsg_2 == mem_gen_cfg[1] ? gen_action(1);
    nextmsg_2 == mem_gen_cfg[2] ? gen_action(2);
    nextmsg_2 == mem_gen_cfg[3] ? gen_action(3); 
    nextmsg_2 == mem_gen_cfg[7] ? gen_action(7);
    nextmsg_2 == mem_gen_cfg[11] ? gen_action(11);
      
    nextmsg_2 < 128 ? g_lastmsg = nextmsg_2;
  );
  nextmsg_offs=-1;
  midirecv(nextmsg_offs,nextmsg_1,nextmsg_2,nextmsg_3) ? midisend(nextmsg_offs,nextmsg_1,nextmsg_2,nextmsg_3);  
);

sidx+=1;
g_chan_selected >= 0 ? (
  g_chan_peak = abs(spl(g_chan_selected));
  g_peakvol = max(g_chan_peak,g_peakvol);
  
  g_recstart_gate && g_firstrec ? (
    g_chan_peak >= g_recstart_gate ? g_recstart_gate=0;
  );
);

spl0=ch1.process(spl0);
spl1=ch2.process(spl1);
spl2=ch3.process(spl2);
spl3=ch4.process(spl3);
spl4=ch5.process(spl4);
spl5=ch6.process(spl5);
spl6=ch7.process(spl6);
spl7=ch8.process(spl7);

g_recstart_gate==0 ? ( 
  g_click_int > 0 && g_active_cnt > 0 && g_length > 0 && !g_firstrec ? (
    spl9 = generate_click();
  );

  (g_pos += 1) >= g_length ? (
    g_firstrec ? (
       g_pos >= g_maxlen ? (
         g_firstrec=0;
         g_pos=0;
       ) : (
         g_length = g_pos+1;
       );
    ) : (
      g_pos=0;
    );
  );
);

g_chan_selected >= 0 ? (
  spl8= spl(g_chan_selected);
) : (
  spl8=0;
);

@gfx 420 480

get_section('gfx');

gfx_clear=-1;

function draw_button(xpos,ypos, w, h, bordersz)
(
  gfx_rect(xpos,ypos,w,h);
  bordersz > 0 ? (
    gfx_set(1,1,1);
    gfx_rect(xpos,ypos,bordersz,h);
    gfx_rect(xpos+bordersz,ypos,w-bordersz*2,bordersz);
    gfx_rect(xpos+w-bordersz,ypos,bordersz,h);
    gfx_rect(xpos+bordersz,ypos+h-bordersz,w-bordersz*2,bordersz);
  );
);

function mouse_in(xpos,ypos,w,h) (
  mouse_x>=xpos && mouse_x <= xpos+w && 
    mouse_y>=ypos && mouse_y <= ypos+h;
);

function draw_speaker(xpos, ypos, w)
(
  xpos -= w*.5;
  gfx_line(xpos,ypos-w*.35,xpos+w*.4,ypos-w*.35);
  gfx_line(xpos,ypos-w*.35,xpos,ypos+w*.35);
  gfx_line(xpos,ypos+w*.35,xpos+w*.4,ypos+w*.35);
  gfx_line(xpos+w*.4,ypos-w*.35,xpos+w*.4,ypos+w*.35);
  
  gfx_line(xpos+w*.4,ypos-w*.35,xpos+w*.8,ypos-w*.7);
  gfx_line(xpos+w*.4,ypos+w*.35,xpos+w*.8,ypos+w*.7);

  gfx_line(xpos+w*.8,ypos-w*.7,xpos+w*.8,ypos+w*.7);
  
);

// draws it right-aligned to xpos, returns width used
function draw_value_tweaker(xpos,ypos,midimem,draw, lbl) local(t w tw th)
(          
  w = 24;
  xpos < 0 ? xpos = (-xpos)-w;
  cap_mode == midimem ? (
    t = ((mouse_y-cap_last_y)*.25);
    t ? (
      lbl == "RDC" ? 
        midimem[0] = min(max(midimem[0]-t*(.1/.25),0),100)
      :
        midimem[0] = min(max(midimem[0]-(t|0),0),128);
      updatechfromrec();
      cap_last_y=mouse_y;
      draw=1;
    );
  );
  cap_mode == 0 &&  mouse_in(xpos,ypos,w,24) ? (
    (mouse_cap & 1) && !(last_mouse_cap&1) ? (  
      midimem >= mem_stlist && midimem < mem_stlist+st_num*g_nchan ? (
        t = (midimem-mem_stlist)%st_num;
        t == st_note1 || t == st_note2 || t == st_note4 ? (
          t != st_note4 || g_latchmode ? (
            g_inject_midinote = 1024+midimem;
          ) : (
            t = ((midimem-mem_stlist)/st_num)|0;
            t == 0 ? ch1.reverse() :
            t == 1 ? ch2.reverse() :
            t == 2 ? ch3.reverse() :
            t == 3 ? ch4.reverse() :
            t == 4 ? ch5.reverse() :
            t == 5 ? ch6.reverse() :
            t == 6 ? ch7.reverse() :
            t == 7 ? ch8.reverse();
          );
        );
      );
    ) : (mouse_cap & 2) && !(last_mouse_cap&2) ? (
      cap_mode = midimem;
      cap_last_y = mouse_y;
    );
  );
  draw ? (
    t=midimem[0];
    gfx_set(.35);
    gfx_rect(xpos,ypos,w,24);
    lbl == "RDC" ? (
      t=floor(t*10)*.1;
      t = t<0.1 ? "0mS" : sprintf(#, t<10?"%.1f":"%d",t);
    ) : (
      t = t > 127 ? "OFF" : sprintf(#, "%d",t);
    );
    gfx_measurestr(t,tw,th);
    gfx_y=ypos + (lbl?(22-th):(24-th)*.5);
    gfx_x=xpos + (w-tw)*.5;
    gfx_set(0.1);
    gfx_drawstr(t);
    lbl ? (
      gfx_set(.5);
      gfx_measurestr(lbl,tw,th);
      gfx_x=xpos+(w-tw)*.5;
      gfx_y=ypos+2;
      gfx_drawstr(lbl);
    );
  );
  w;
);

function draw_waveform(xpos,ycent,w,amp,bptr,srclen) local(minv,maxv, i,v, di, dbptr)
(
  minv=100;
  maxv=-100;
  i=xpos|0;
  dbptr = 1;
  while (srclen > 100000) ( srclen*=0.5; dbptr *= 2.0; );
  di = w / srclen;
  ycent|=0;
  loop(srclen,
    v=bptr[0];
    minv=min(v,minv);
    maxv=max(v,maxv);
    bptr+=dbptr;
    v=0|(i+=di);
    v>xpos ? (
      gfx_line(xpos=v,ycent+amp*max(min(minv,1),-1),v,ycent+amp*max(min(maxv,1),-1),0);
      minv=100; maxv=-100;
    );
  );
);

function draw(xpos, ypos) local(i w h gap rec mode force_redraw rowsize mx my mw t nrows lx ly wantr tw th now)
(
  force_redraw = gfx_w != last_w || gfx_h != last_h;
  
  gap = 4;
  rowsize=4;
  nrows = 4;
  w=0|min(
    ((gfx_w - 2 - gap*(rowsize-1))/rowsize),
    ((gfx_h - ypos - 8 - gap*(nrows-1))/nrows));
  
  h=w;
  i=0;
  rec = mem_stlist;
  now=time_precise();
  xpos += 2;
  loop(g_nchan,
    i>0 && (i%rowsize) == 0 ? (
      xpos=2;
      ypos += h + gap;
    );
    
    !cap_mode && (mouse_cap&1) && mouse_in(xpos,ypos,w,h) ? (
       g_chan_selected=i;
    );    
    mode = rec[st_state];
    mode ? (
      gfx_set(mode==2 ? 1 : 0.25,mode==1?1:0.25, 0.25);
    ) : (
      rec[st_dirty] ? gfx_set(0.3,0.3,0.4) : gfx_set(0.25);
    );
    g_chan_selected == i ? mode|=8;
    rec[st_dirty] ? mode|=16;
    mode |= rec[st_monmode] << 8;
    g_firstrec ? mode |= 1<<30;
    mw = (w*.2)|0;
    mx = xpos + w - mw - gap;
    my = ypos+gap;
  
    force_redraw || rec[st_lastd] != mode || ((mode&2) && now>rec[st_lastpkupd]+0.5) ? (
      rec[st_lastd] = mode;
      rec[st_lastpkupd]=now;
      draw_button(xpos,ypos,w,h,g_chan_selected == i ? gap : 0);

      gfx_set(1,1,1);
      gfx_x = xpos + 8;
      gfx_y = ypos + 8;
      gfx_printf("%d",i+1);    

      gfx_set(0.5);
      g_length && rec[st_dirty] && w > 80 ? draw_waveform(xpos+gap,ypos+h*.5,w-2*gap,h*.25,rec[st_buf]+g_offs,g_length);
      
      rec[st_monmode]==0 ? (
        gfx_set(0.2)
      ) : rec[st_monmode]==1 ? (
        (mode&8) ? gfx_set(0.3,0.3,1.0) : gfx_set(0.3,0.3,0.7) 
      ) : (
        gfx_set(0.9,0.8,0);
      );
      
      gfx_rect(mx,my,mw,mw);
      
      rec[st_monmode] == 0 ? (
        gfx_set(0);
      ) : (
        rec[st_monmode]==2 || (mode&8) ? (       
          gfx_set(1,1,1, 1);
        ) : (
          gfx_set(1,1,1, .25);
        );
      );
      draw_speaker(mx+mw/2,my+mw/2,mw/2);    
        
      rec[st_monmode] == 0 ? (
        gfx_set(0);
        gfx_line(mx,my,mx+mw,my+mw);
        gfx_line(mx+mw,my,mx,my+mw);
      );      
         
      wantr=1;
    ) : (
      wantr=0;
    );
    t = mem_stlist + i*st_num;
    lx = xpos + w - gap;
    ly = ypos+h-24-gap;
    w > 80 ? ( lx -= draw_value_tweaker(-lx,ly,t + st_note3,wantr, "mon") + gap; );
    lx -= draw_value_tweaker(-lx,ly,t + st_note2,wantr, rec[st_state] == 1 ? "stp" : "ply") + gap;
    lx -= draw_value_tweaker(-lx,ly,t + st_note1,wantr, rec[st_state] == 2 && !g_firstrec ? "ply" : "rec") + gap;
    
    draw_value_tweaker(-mx+gap,my,t + st_rdc, wantr, "RDC"); 

    w > 80 ? (
      lx = xpos+gap;
      w < 120 ? ly = ypos+h-48-gap*2;
      lx -= draw_value_tweaker(xpos+gap,ly,t + st_note4,wantr, "rev") + gap;    
    );
    
    (mouse_cap&2) && !(last_mouse_cap&2) && !cap_mode && mouse_in(mx,my,mw,mw) ? (
      rec[st_monmode] = (rec[st_monmode]+1)%3;
      updatechfromrec();
      sliderchange(-1);
    );
    
    g_curpk = (max(min(log10(rec[st_peak_in]) * 20,0),-68) * w * (1/68) + w);
    g_curpk = g_curpk > (10^-5) && g_curpk<3 ? 3 : (g_curpk|0);
    
    gfx_set(0);
    gfx_rect(xpos-2,ypos,2,w-g_curpk);
    gfx_set(.75,.75,0);
    gfx_rect(xpos-2,ypos+w-g_curpk,2,g_curpk);
    
    xpos += w + gap;
    i+=1;
    rec += st_num;
  );
  xpos = 2;
  ypos += h+gap;

  t=0;
  loop(mem_gen_sz,
    xpos >= gfx_w-4 || t==4 ? (
      xpos=2;
      ypos += h + gap;
    );
    i=mem_gen_order[t];
    
    i>=0 ? (
      force_redraw ? (
        i == 1 ? gfx_set(0.0, 0.5, 0.6) :
        i == 2 ? gfx_set(0.0, 0.6, 0.5) : 
        i == 3 ? gfx_set(0.0, 0.6, 0.4) : 
        i == 7 ? gfx_set(0.7, 0.6, 0.7) :
        i == 11 ? gfx_set(0,0.5,0.5) :
                 gfx_set(0.5, 0.0, 0.0);
          
        draw_button(xpos,ypos,w,h,0);
        gfx_set(1);
        gfx_measurestr(mem_gen_names[i],tw,th);
        gfx_x=xpos+(w-tw)*.5;
        gfx_y=ypos+(h-th)*.5;
        gfx_drawstr(mem_gen_names[i]);
      );
      draw_value_tweaker(-(xpos+w-gap),ypos+h-24-gap,mem_gen_cfg + i, force_redraw, 0);
      
      !(last_mouse_cap&1) && (mouse_cap&1) && mouse_in(xpos,ypos,w,h) ? (
        gen_action(i);
      );
        
      xpos += w+gap;
    );
    t+=1;
  );  
);

function draw_position(ypos) local(last_pos,last_len)
(
  last_w != gfx_w || last_h != gfx_h || last_pos != g_pos || last_len != g_length ? (
    last_len = g_length;
    last_pos = g_pos;
    gfx_set(0.25,0.125,0.0);
    gfx_rect(0,ypos,gfx_w,20);
    gfx_set(0.125,0.25,0.5);
    gfx_rect(0,ypos,g_pos/g_length * gfx_w,20);
    gfx_x=2;
    gfx_y=ypos+2;
    gfx_set(1,1,1);
    gfx_printf("%d mS\n%.1f BPM",g_length/srate * 1000,estbpm(last_len));
  );
);

function draw_topbar_button(xp, yp, str) 
  instance(w,h,x,y) 
  globals(gfx_x,gfx_y) 
(
  gfx_measurestr(str, w, h);
  x=xp;
  y=yp;
  w+=3;
  h+=3;
  gfx_set(.5,.7,1);
  gfx_line(x,y,x+w,y);
  gfx_line(x+w,y,x+w,y+h);
  gfx_line(x,y+h,x+w,y+h);
  gfx_line(x,y,x,y+h);
  h+=1;
  w+=1;
  gfx_x=xp+2; gfx_y=yp+2;
  gfx_drawstr(str);
  gfx_x = xp+w;
);
function hit_topbar_button(xp,yp,cm) 
  instance(w,h,x,y) 
  globals(cap_mode, cap_last_x, cap_last_y) 
( 
  xp>=x&&yp>=y&&xp<x+w&&yp<y+h ? (
    cap_last_x = xp;
    cap_last_y = yp;
    cap_mode=cm;
  );
);

function draw_topbar(need_ref) local(s,i,t, lastlen, lastgate, lastlatch,lasthalve,
        lastclickcount, lastvclickoffs, lastoffs)
(
//need_ref=1;
  need_ref == 0 ? (
    need_ref = (lastlen != g_length || 
                lastoffs != g_offs ||
                lastgate != mem_gen_cfg[4] || 
                lastlatch != g_latchmode ||
                lastclickcount != (mem_gen_cfg[9]|0) ||
                lastvclickoffs != mem_gen_cfg[10] ||
                lasthalve != mem_gen_cfg[5]);
  );

  need_ref ? (
    gfx_set(0,0,0.3);
    gfx_rect(0,0,gfx_w,14);

    gate_button.draw_topbar_button(64,2,
                 mem_gen_cfg[4] < g_infthreshdb ? "gate: -inf dB" :
                 sprintf(#,"gate: %.0fdB",lastgate=mem_gen_cfg[4]));
    latch_button.draw_topbar_button(gfx_x+4,2,
                  (lastlatch=g_latchmode) ? "latch on" : "latch off");

    offs_button.draw_topbar_button(gfx_x+4,2,
                  sprintf(#,"offs %.0f spls", lastoffs=g_offs));

    length_button.draw_topbar_button(gfx_x+4,2,
                  sprintf(#,"%.0f spls", lastlen=g_length));
                  
    clickcnt_button.draw_topbar_button(gfx_x+4,2,
                  sprintf(#,"click cnt: %d",lastclickcount=(mem_gen_cfg[9]|0)));
                  
    lastclickcount>0 ? vclick_button.draw_topbar_button(gfx_x+4,2,
                  (lastvclickoffs=mem_gen_cfg[10])>0 ? sprintf(#,"vclick: +%.1fmS",lastvclickoffs): 
                  "vclick: off");                 
                
    fadesz_button.draw_topbar_button(gfx_x+4,2,
                  sprintf(#,"halve fade: %.0f mS",lasthalve=mem_gen_cfg[5]));
  );
  
  (mouse_cap&1) && !(mouse_cap&2) ? ( 
    !(last_mouse_cap&1) ? (
    
      latch_button.hit_topbar_button(mouse_x,mouse_y,-1) ? (
        // toggle
        mem_gen_cfg[8] = g_latchmode = !g_latchmode;
      ) : (
        gate_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+4)||
        length_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+6)||
        offs_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+12)||
        clickcnt_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+9)||
        (lastclickcount&&vclick_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+10))||
        fadesz_button.hit_topbar_button(mouse_x,mouse_y,mem_gen_cfg+5);
      );
    ) : (mouse_y != cap_last_y && cap_mode) ? (
      cap_mode == mem_gen_cfg + 4 ? (
        mem_gen_cfg[4] = min(max(mem_gen_cfg[4]-(mouse_y-cap_last_y)*.2,g_infthreshdb),-1);        
        cap_last_y=mouse_y;
      ) : cap_mode == mem_gen_cfg + 6 ? (
        t = ((mouse_y-cap_last_y)*((mouse_cap&8)?4:100))|0;
        t ? (
          g_length = max(g_length-t,1);
          g_pos %= g_length;
          g_edgetrim_drawstate=1;
          cap_last_y=mouse_y;
        );       
      ) : cap_mode == mem_gen_cfg + 12 ? (
        t = ((mouse_y-cap_last_y)*((mouse_cap&8)?4:100))|0;
        t ? (
          i = g_offs+g_length;
          g_offs = min(max(g_offs-t,0),i-1);
          t = (i-g_length) - g_offs;
          g_length = i - g_offs;
          g_pos = (g_pos + t) % g_length;
          g_edgetrim_drawstate=1;
          cap_last_y=mouse_y;
        );       
      ) : cap_mode == mem_gen_cfg + 5 ? (
        mem_gen_cfg[5] = min(max(mem_gen_cfg[5]-(mouse_y-cap_last_y)*0.05,0),500);
        cap_last_y=mouse_y;
      ) : cap_mode == mem_gen_cfg + 9 ? (
        mem_gen_cfg[9] = min(max(mem_gen_cfg[9]-(mouse_y-cap_last_y)*0.05,0),64);
        cap_last_y=mouse_y;
      ) : cap_mode == mem_gen_cfg + 10 ? (
        mem_gen_cfg[10] = min(max(mem_gen_cfg[10]-(mouse_y-cap_last_y)*0.05,0),100);
        cap_last_y=mouse_y;
      );
    );
  );
  
  
  // animated logo
  gfx_x=0;
  gfx_y=4;
  t=time_precise();
  s="Super8";
  i=0;
  loop(strlen(s),
    gfx_set(sin(t+i*0.2)*0.3+0.5, cos(t*.61+i*0.5)*.7+0.3, sin(t*2.1-i*0.6)*.3+0.7);
    gfx_drawchar(str_getchar(s,i));
    i+=1;
  );
);

function get_vclick_pos(latms, clickcnt) 
  global(g_length, g_pos, srate, samplesblock)
  local(tmp)
(
  clickcnt >= 1 ? (
    tmp = g_pos - latms*srate*0.001;
    tmp < 0 ? tmp += g_length;
    (tmp >= 0) ? (
      floor(g_pos * clickcnt / g_length)
    ) : -1;
  ) : -1;
);

vclick_pos = mem_gen_cfg[10]>0 && g_active_cnt && !g_firstrec  ? get_vclick_pos(mem_gen_cfg[10],mem_gen_cfg[9]|0) : -1;

last_vclick_pos != vclick_pos ? (
  last_w=0;
  last_vclick_pos=vclick_pos;
);

last_w != gfx_w || last_h != gfx_h ? (
 gfx_set(0.125);
 gfx_rect(0,0,gfx_w,gfx_h);
);

draw_topbar(last_w != gfx_w || last_h != gfx_h);
draw_position(15);
draw(0, 40);

gfx_x=0;
gfx_y=gfx_h-8;
gfx_set(0);
gfx_rect(gfx_x,gfx_y,6*8,8);
gfx_set(.3);
g_peakvol < g_infthresh ? (
  gfx_printf("-inf");
) : (
  gfx_printf("%.0fdB",log10(g_peakvol)*20);
  g_peakvol *= 0.93;
);


g_lastmsg>=0 && g_lastmsg_draw!=g_lastmsg ? (
  gfx_x=gfx_w-24;
  gfx_y=gfx_h-8;
  gfx_set(0);
  gfx_rect(gfx_x,gfx_y,24,8);
  gfx_set(.3);
  gfx_x += g_lastmsg<10?16:g_lastmsg<100?8;
  gfx_printf("%d",g_lastmsg);
  g_lastmsg_draw=g_lastmsg;
);

last_w = gfx_w;
last_h = gfx_h;
last_mouse_cap = mouse_cap;
0 == (mouse_cap & 3) ? (
  cap_mode > 0 ? (
    sliderchange(-1);
  );
  cap_mode = 0;
);

function draw_vclick(pos) 
  global(gfx_w,gfx_h,gfx_x,gfx_y)
  local(sw,sh,str)
(
  gfx_setfont(2,"Arial",min(gfx_h,gfx_w)*.5);
  str=sprintf(#,"%d",1 + pos);
  gfx_measurestr(str,sw,sh);

  gfx_x=gfx_w*.5 + ((pos&1)?sw*.3:-sw*1.3);
  gfx_y=gfx_h*.5 + ((pos&2)?0:-sh);
  gfx_set(1);
  gfx_drawstr(str);
  gfx_setfont(0);
);

vclick_pos >= 0 ? draw_vclick(vclick_pos);

function draw_trim_waveform(xpos, ypos, w, h, chan, isend)
  global(g_length mem_stlist st_num st_dirty st_buf g_offs srate) 
  local(rec buf len maxl linepos xpos2 w2)
(
  rec = mem_stlist + chan*st_num;
  gfx_set(0.25);
  gfx_rect(xpos,ypos,w,h);
  
  gfx_set(1);
  len = (w * 0.0005 * srate)|0; // 0.5ms/pixel
  xpos2 = xpos;
  w2=w;

  isend ? (
    buf = g_offs + g_length - len*.8;
  ) : (
    buf = g_offs - len*.2;
  );

  buf < 0 ? (
    xpos2 -= buf*w/len;
    len += buf;
    w2 -= xpos2-xpos;
    buf = 0;
  );
  
  buf += rec[st_buf];
  
  
  g_length && rec[st_dirty] ? 
    draw_waveform(xpos2,ypos+h*.5,w2,h*.25,buf,len);
    
  gfx_set(0,.5,.7,.5);
  xpos += (w*(isend?.8:.2))|0;
  gfx_line(xpos,ypos,xpos,ypos+h);
);

cap_mode == mem_gen_cfg + 12 || cap_mode == mem_gen_cfg + 6 ? (
  g_edgetrim_drawstate>=0 ? (
    draw_trim_waveform(0,40,gfx_w,gfx_h-40, max(g_chan_selected,0), cap_mode == mem_gen_cfg+6);
    g_edgetrim_drawstate=1;
  );
) : (
  g_edgetrim_drawstate>0 ? (
    last_w=-1;
    g_edgetrim_drawstate=0;
  );  
);

// export must happen from UI thread
function export()  local(rec, flag)
(
  // export, yay reaper 5.05 undocumented function!
  rec = mem_stlist;
  flag=1<<16; // go to end of project
  flag|=2<<16; // use tempo from next parameter
  g_length > 0 ? loop(g_nchan,
    rec[st_state] ? (
      export_buffer_to_project(rec[st_buf]+g_offs,g_length,1,srate,(rec-mem_stlist)/st_num, flag, flag?estbpm(g_length));
      flag=0; // only go to end/set tempo for first item
    );
    rec+=st_num;
  );
);

g_need_export ? (
  export();
  g_need_export = 0;
);

